a tool for shared writing and social publishing
1"use client";
2import { AtUri } from "@atproto/syntax";
3import { PubLeafletDocument } from "lexicons/api";
4import { EditTiny } from "components/Icons/EditTiny";
5
6import { usePublicationData } from "./PublicationSWRProvider";
7import { Fragment, useState } from "react";
8import { useParams } from "next/navigation";
9import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
10import { Menu, MenuItem } from "components/Layout";
11import { deletePost } from "./deletePost";
12import { ButtonPrimary } from "components/Buttons";
13import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny";
14import { DeleteSmall } from "components/Icons/DeleteSmall";
15import { ShareSmall } from "components/Icons/ShareSmall";
16import { ShareButton } from "components/ShareOptions";
17import { SpeedyLink } from "components/SpeedyLink";
18import { QuoteTiny } from "components/Icons/QuoteTiny";
19import { CommentTiny } from "components/Icons/CommentTiny";
20import { useLocalizedDate } from "src/hooks/useLocalizedDate";
21
22export function PublishedPostsList(props: {
23 searchValue: string;
24 showPageBackground: boolean;
25}) {
26 let { data } = usePublicationData();
27 let params = useParams();
28 let { publication } = data!;
29 if (!publication) return null;
30 if (publication.documents_in_publications.length === 0)
31 return (
32 <div className="italic text-tertiary w-full container text-center place-items-center flex flex-col gap-3 p-3">
33 Nothing's been published yet...
34 </div>
35 );
36 return (
37 <div className="publishedList w-full flex flex-col gap-2 pb-4">
38 {publication.documents_in_publications
39 .sort((a, b) => {
40 let aRecord = a.documents?.data! as PubLeafletDocument.Record;
41 let bRecord = b.documents?.data! as PubLeafletDocument.Record;
42 const aDate = aRecord.publishedAt
43 ? new Date(aRecord.publishedAt)
44 : new Date(0);
45 const bDate = bRecord.publishedAt
46 ? new Date(bRecord.publishedAt)
47 : new Date(0);
48 return bDate.getTime() - aDate.getTime(); // Sort by most recent first
49 })
50 .map((doc) => {
51 if (!doc.documents) return null;
52 let leaflet = publication.leaflets_in_publications.find(
53 (l) => doc.documents && l.doc === doc.documents.uri,
54 );
55 let uri = new AtUri(doc.documents.uri);
56 let record = doc.documents.data as PubLeafletDocument.Record;
57 let quotes = doc.documents.document_mentions_in_bsky[0]?.count || 0;
58 let comments = doc.documents.comments_on_documents[0]?.count || 0;
59
60 return (
61 <Fragment key={doc.documents?.uri}>
62 <div className="flex gap-2 w-full ">
63 <div
64 className={`publishedPost grow flex flex-col hover:no-underline! rounded-lg border ${props.showPageBackground ? "border-border-light py-1 px-2" : "border-transparent px-1"}`}
65 style={{
66 backgroundColor: props.showPageBackground
67 ? "rgba(var(--bg-page), var(--bg-page-alpha))"
68 : "transparent",
69 }}
70 >
71 <div className="flex justify-between gap-2">
72 <a
73 className="hover:no-underline!"
74 target="_blank"
75 href={`${getPublicationURL(publication)}/${uri.rkey}`}
76 >
77 <h3 className="text-primary grow leading-snug">
78 {record.title}
79 </h3>
80 </a>
81 <div className="flex justify-start align-top flex-row gap-1">
82 {leaflet && (
83 <SpeedyLink
84 className="pt-[6px]"
85 href={`/${leaflet.leaflet}`}
86 >
87 <EditTiny />
88 </SpeedyLink>
89 )}
90 <Options document_uri={doc.documents.uri} />
91 </div>
92 </div>
93
94 {record.description ? (
95 <p className="italic text-secondary">
96 {record.description}
97 </p>
98 ) : null}
99 <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-3">
100 {record.publishedAt ? (
101 <PublishedDate dateString={record.publishedAt} />
102 ) : null}
103 {(comments > 0 || quotes > 0) && record.publishedAt
104 ? " | "
105 : ""}
106 {quotes > 0 && (
107 <SpeedyLink
108 href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=quotes`}
109 className="flex flex-row gap-1 text-sm text-tertiary items-center"
110 >
111 <QuoteTiny /> {quotes}
112 </SpeedyLink>
113 )}
114 {comments > 0 && quotes > 0 ? " " : ""}
115 {comments > 0 && (
116 <SpeedyLink
117 href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=comments`}
118 className="flex flex-row gap-1 text-sm text-tertiary items-center"
119 >
120 <CommentTiny /> {comments}
121 </SpeedyLink>
122 )}
123 </div>
124 </div>
125 </div>
126 {!props.showPageBackground && (
127 <hr className="last:hidden border-border-light" />
128 )}
129 </Fragment>
130 );
131 })}
132 </div>
133 );
134}
135
136let Options = (props: { document_uri: string }) => {
137 return (
138 <Menu
139 align="end"
140 alignOffset={20}
141 asChild
142 trigger={
143 <button className="text-secondary rounded-md selected-outline border-transparent! hover:border-border! h-min">
144 <MoreOptionsVerticalTiny />
145 </button>
146 }
147 >
148 <>
149 <OptionsMenu document_uri={props.document_uri} />
150 </>
151 </Menu>
152 );
153};
154
155function OptionsMenu(props: { document_uri: string }) {
156 let { mutate, data } = usePublicationData();
157 let [state, setState] = useState<"normal" | "confirm">("normal");
158
159 let postLink = data?.publication
160 ? `${getPublicationURL(data?.publication)}/${new AtUri(props.document_uri).rkey}`
161 : null;
162
163 if (state === "normal") {
164 return (
165 <>
166 <ShareButton
167 className="justify-end"
168 text={
169 <div className="flex gap-2">
170 Share Post Link
171 <ShareSmall />
172 </div>
173 }
174 subtext=""
175 smokerText="Post link copied!"
176 id="get-post-link"
177 fullLink={postLink?.includes("https") ? postLink : undefined}
178 link={postLink}
179 />
180
181 <hr className="border-border-light" />
182 <MenuItem
183 className="justify-end"
184 onSelect={async (e) => {
185 e.preventDefault();
186 setState("confirm");
187 return;
188 }}
189 >
190 Delete Post
191 <DeleteSmall />
192 </MenuItem>
193 </>
194 );
195 }
196 if (state === "confirm") {
197 return (
198 <div className="flex flex-col items-center font-bold text-secondary px-2 py-1">
199 Are you sure?
200 <div className="text-sm text-tertiary font-normal">
201 This action cannot be undone!
202 </div>
203 <ButtonPrimary
204 className="mt-2"
205 onClick={async () => {
206 await mutate((data) => {
207 if (!data) return data;
208 return {
209 ...data,
210 publication: {
211 ...data.publication!,
212 leaflets_in_publications:
213 data.publication?.leaflets_in_publications.filter(
214 (l) => l.doc !== props.document_uri,
215 ) || [],
216 documents_in_publications:
217 data.publication?.documents_in_publications.filter(
218 (d) => d.documents?.uri !== props.document_uri,
219 ) || [],
220 },
221 };
222 }, false);
223 await deletePost(props.document_uri);
224 }}
225 >
226 Delete
227 </ButtonPrimary>
228 </div>
229 );
230 }
231}
232
233function PublishedDate(props: { dateString: string }) {
234 const formattedDate = useLocalizedDate(props.dateString, {
235 year: "numeric",
236 month: "long",
237 day: "2-digit",
238 });
239
240 return (
241 <p className="text-sm text-tertiary">
242 Published {formattedDate}
243 </p>
244 );
245}